package transport;

import java.util.zip.CRC32;

public class TransportPacket {

	/**
	 * The source adress, 4 bits\
	 * @invariant 0 <= source <= Transport.Constants.PORT_MAX
	 */
	byte source;
	/**
	 * The destination adress, 4 bits
	 * @invariant 0 <= dest <= Transport.Constants.PORT_MAX
	 */
	byte dest;
	/**
	 * The sequence number of this packet, 1 byte wide
	 * NOTE: only the bottom byte of this char is used. However it is not necesarry
	 * to wrap the value from 255 to 0 as this happens automaticly when discarding the top byte of anything >= 256
	 * @invariant 0 <= seq <= Transport.Constants.MAX_SEQUENCE_NUMBER
	 */
	char seq;
	/**
	 * the ack-number of this adress, only valid when isAck()
	 * NOTE: only the bottom byte of this char is used. However it is not necesarry
	 * to wrap the value from 255 to 0 as this happens automaticly when discarding the top byte of anything >= 256
	 * @invariant 0 <= ackseq <= Transport.Constants.MAX_SEQUENCE_NUMBER
	 */
	char ackseq;
	/**
	 * If the ACK-bit of this packet is set
	 */
	boolean ack;
	/**
	 * if the SYN-bit of this packet is set
	 */
	boolean syn;
	/**
	 * if the FIN bit of this packet is set
	 */
	boolean fin;
	/**
	 * The 32 bit CRC checksum of this packet
	 * @invariant 0 <= checksum < 2^32
	 */
	private long checksum;
	/**
	 * The byte array containing the actual payload
	 * @invariant 0 <= data.length < Transport.Constants.MAX_PAYLOAD_LENGTH
	 */
	byte[] data;

	/**
	 * creates a new Tranport packet
	 * @param src the source adress
	 * @param dst the destination adress
	 * @param sequence the sequence number
	 * @param acksequence the ack number
	 * @param ack ACK-bit
	 * @param syn SYN-bit
	 * @param fin FIN-bit
	 * @param data the payload
	 * @requires 0<=src<=Transport.Constants.PORT_MAX
	 * @requires 0<=dst<=Transport.Constants.PORT_MAX
	 * @requires sequence <= Transport.Constants.MAX_SEQUENCE_NUMBER
	 * @requires acksequence <= Transport.Constants.MAX_SEQUENCE_NUMBER
	 * @requires 0<=data.length<=Transport.Constants.MAX_PAYLOAD_LENGTH
	 */
	public TransportPacket(byte src, byte dst, char sequence, char acksequence,
			boolean ack, boolean syn, boolean fin, byte[] data) {
		if(src > Constants.PORT_MAX || src < 0)
			throw new IllegalArgumentException("src not in range 0 <= src <= "+Constants.PORT_MAX+" value="+src);
		this.source = src;
		
		if(dst > Constants.PORT_MAX || dst < 0)
			throw new IllegalArgumentException("dst not in range 0 <= dst <= "+Constants.PORT_MAX+" value="+dst);
		this.dest = dst;
		
		if(sequence > Constants.MAX_SEQUENCE_NUMBER)
			throw new IllegalArgumentException("sequence not in range sequence <= "+Constants.MAX_SEQUENCE_NUMBER+" value="+sequence);
		this.seq = sequence;
		
		if(acksequence > Constants.MAX_SEQUENCE_NUMBER)
			throw new IllegalArgumentException("acksequence not in range acksequence <= "+Constants.MAX_SEQUENCE_NUMBER+" value="+acksequence);
		this.ackseq = acksequence;
		
		this.ack = ack;
		this.syn = syn;
		this.fin = fin;
		
		if(data.length > Constants.MAX_PAYLOAD_LENGTH || data.length < 0)
			throw new IllegalArgumentException("Payload length greater then MAX_PAYLOAD_LENGTH ("+data.length+" > "+Constants.MAX_PAYLOAD_LENGTH+")");
		this.data = data;
		
		createChecksum();
	}
	
	
	public boolean CheckChecksum(){
		return this.checksum == calcChecksum();
	}	

	public byte[] toByteArray() {
		byte[] res = new byte[Constants.HEADER_LENGTH + data.length];
		res[0] = getPortByte();
		res[1] = getSeqAsByte();
		res[2] = getAckSeqAsByte();
		res[3] = getOptionsByte();
		
		long chks = this.checksum;
		res[7] = (byte) chks;
		res[6] = (byte) (chks >> 8);
		res[5] = (byte) (chks >> 16);
		res[4] = (byte) (chks >> 24);
		
		System.arraycopy(data, 0, res, 8, data.length);
		
		return res;
	}
	
	protected void createChecksum(){
		this.checksum = calcChecksum();
	}
	
	private byte getPortByte(){
		//set right 4 bits to dest
		byte res = (byte) (dest & 0x0F);
		res |= (byte) ((source & 0x0F) << 4);		
		return res;
	}
	
	private byte getOptionsByte(){
		byte res = 0x00;
		if(ack)
			res |= 0x80;
		if(syn)
			res |= 0x40;
		if(fin)
			res |= 0x20;
		//If you comment out the section below this the 5 free bits 
		//are filled with random data
		/*for(int i=0; i<5; i++){
			if(Math.random() < 0.5)
				res |= (0x01 << i);
		}*/
		return res;
	}
	
	private byte getSeqAsByte(){
		return (byte) (((byte) seq) & 0x00FF);
	}
	
	private byte getAckSeqAsByte(){
		return (byte) (((byte) ackseq) & 0x00FF);
	}
	
	private long calcChecksum(){
		byte[] check = new byte[4 + data.length];
		check[0] = getPortByte();
		check[1] = getSeqAsByte();
		check[2] = getAckSeqAsByte();
		check[3] = getOptionsByte();
		System.arraycopy(data, 0, check, 4, data.length);
		
		CRC32 crc = new CRC32();
		crc.update(check);
		
		return crc.getValue();
	}
	
	
	public byte getSource() {
		return source;
	}
	public byte getDest() {
		return dest;
	}
	public char getSeq() {
		return seq;
	}
	public char getAckseq() {
		return ackseq;
	}
	public boolean isAck() {
		return ack;
	}
	public boolean isSyn() {
		return syn;
	}
	public boolean isFin() {
		return fin;
	}
	public byte[] getData() {
		return data;
	}
	protected void setChecksum(long checksum) {
		this.checksum = checksum;
	}
	/**
	 * For testing purposes only!
	 * @return the checksum
	 */
	public long getChecksum(){
		return this.checksum;
	}

	public static TransportPacket fromByteArray(byte[] pack){
		//first parse the port byte
		//source is left 4 bits, dest is bottom 4 bits
		byte source = (byte) ((pack[0] >> 4) & 0x0F);
		byte dest = (byte) (pack[0] & 0x0F);
		// seq and ackseq
		int seq = (char) (((char) pack[1]) & 0x00FF);
		int ackseq = (char) (((char) pack[2]) & 0x00FF);
		//the options
		boolean ack = ( (byte) (pack[3] & 0x80) ) == (byte) 0x80;
		boolean syn = ( (byte) (pack[3] & 0x40) ) == (byte) 0x40;
		boolean fin = ( (byte) (pack[3] & 0x20) ) == (byte) 0x20;
		//the checksum
		long chks = 0;
		chks += ((pack[4] << 24) & 0x00000000FF000000L);
		chks += ((pack[5] << 16) & 0x0000000000FF0000L);
		chks += ((pack[6] << 8) & 0x000000000000FF00L);
		chks += ((pack[7]) & 0x00000000000000FFL);
		//the data
		byte[] data = new byte[pack.length - Constants.HEADER_LENGTH];
		System.arraycopy(pack, 8, data, 0, data.length);
		
		TransportPacket res = new TransportPacket(source, dest, (char)seq, (char)ackseq, ack, syn, fin, data);
		res.setChecksum(chks);
		
		return res;
	}
	
	
	
}
